using System;
using System.Collections.Generic;
using System.Reflection;
#if TEST
using Fadd.Commands.Tests;
using Xunit;
#endif

namespace Fadd.Commands
{
	/// <summary>
	/// Class for building information strings about a proxy method
	/// </summary>
	public class ObjectProxyMethod
	{
		private readonly static ObjectProxyTypeNameGenerator _typeNameGenerator = new ObjectProxyTypeNameGenerator();

		#region Method declaration output string: string MethodDeclaration and string MethodVoidDeclaration

		private const string MethodVoidDeclaration =
@"
		public {virtual} void {genericMethodName}({arguments})
		{
			ObjectProxyCommand cmd = new ObjectProxyCommand(_realType, {methodSignature}, {argumentArray});
			_dispatcher.Invoke(this, cmd);
			if (!cmd.IsHandled)
				throw new NotImplementedException(""{fullName}.{methodName} have not been implemented."");
			if (cmd.Exception != null)
				throw cmd.Exception;
		}";

		private const string MethodDeclaration = 
@"
		public {virtual} {returnType} {genericMethodName}({arguments})
		{
			ObjectProxyCommand cmd = new ObjectProxyCommand(_realType, {methodSignature}, {argumentArray});
			_dispatcher.Invoke(this, cmd);
			if (!cmd.IsHandled)
				throw new NotImplementedException(""{fullName}.{methodName} have not been implemented."");
			if (cmd.Exception != null)
				throw cmd.Exception;

			return ({returnType})cmd.ReturnValue;
		}";

		#endregion

		private readonly MethodInfo _methodInfo;
		private readonly string _name;
		private readonly string _signature;
		private readonly string _returnType;
		private readonly string _declaration;
		private readonly string _getMethodCall;
		private readonly List<Type> _neededTypes = new List<Type>();

		/// <summary>
		/// Instantiates a new object to parse and create readable data for a method
		/// </summary>
		/// <param name="methodInfo"></param>
		public ObjectProxyMethod(MethodInfo methodInfo)
		{
			Check.Require(methodInfo, "methodInfo");
			
			_methodInfo = methodInfo;
			_name = _methodInfo.Name.Replace("`" + _methodInfo.GetGenericArguments().Length, string.Empty);
			_returnType = _typeNameGenerator.GetGenericName(_methodInfo.ReturnType);
			_getMethodCall = GenerateGetMethodCall(methodInfo, _name);

			if(_methodInfo.ReturnType != typeof(void))
				_neededTypes.Add(_methodInfo.ReturnType);

			// Build a _lowerCaseName
			string typedName = GetTypedName(_methodInfo, _name);
			_signature = ("_" + typedName[0]).ToLower() + typedName.Substring(1);
			
			// Build the declaration of the method
			_declaration = BuildDeclaration(this);
		}

		private static string BuildDeclaration(ObjectProxyMethod method)
		{
			string declaration;
			if (method._returnType == "Void")
				declaration = MethodVoidDeclaration;
			else
				declaration = MethodDeclaration.Replace("{returnType}", method._returnType);

			declaration = declaration.Replace("{virtual}", method._methodInfo.DeclaringType.IsInterface ? string.Empty : "override");
			declaration = declaration.Replace("{methodName}", method._name);
			declaration = declaration.Replace("{genericMethodName}", GetGenericName(method._methodInfo, method._name));
			declaration = declaration.Replace("{methodSignature}", method._signature);
			declaration = declaration.Replace("{arguments}", GetArgumentDeclaration(method._methodInfo, method._neededTypes));
			declaration = declaration.Replace("{fullName}", _typeNameGenerator.GetFullName(method._methodInfo.DeclaringType));
			declaration = declaration.Replace("{argumentArray}", GetArgumentArray(method._methodInfo));

			return declaration;
		}

		#region Helpers for generating member data or for building the declaration

		/// <summary>
		/// <![CDATA[
		/// Returns a full generic name if needed, ie GetThings<P, Q>
		/// ]]>
		/// </summary>
		/// <param name="info">The method to generate the name for</param>
		/// <param name="name">The methods name</param>
		private static string GetGenericName(MethodInfo info, string name)
		{
			string result = name;

			if(info.IsGenericMethod)
			{
				result += "<";
				foreach (Type type in info.GetGenericArguments())
					result += type.Name + ", ";

				result = result.TrimEnd(',', ' ') + ">";
			}

			return result;
		}

		/// <summary>
		/// Joins method name together with all parameter types creating an identifying string for the method
		/// </summary>
		/// <param name="info">The method to generate the name for</param>
		/// <param name="methodName">The method name</param>		
		private static string GetTypedName(MethodInfo info, string methodName)
		{
			string name = methodName;
			foreach (ParameterInfo parameter in info.GetParameters())
				name += _typeNameGenerator.GetTypedName(parameter.ParameterType);

			return name;
		}

		/// <summary>
		/// Retrieves the call to Type.GetMethod needed to retrieve the method
		/// </summary>
		/// <param name="info">The method to retrieve</param>
		/// <param name="methodName">The correct name of the method to retrieve</param>
		/// <returns>
		/// ie. GetMethod("GetUser", new Type[]{typeof(Int32)})
		/// </returns>
		private static string GenerateGetMethodCall(MethodInfo info, string methodName)
		{
			string call = "GetMethod(\"" + methodName + "\", new Type[]{";

			foreach (ParameterInfo parameterInfo in info.GetParameters())
				call += "typeof(" + _typeNameGenerator.GetGenericName(parameterInfo.ParameterType) + "), ";

			return call.TrimEnd(',', ' ') + "});";
		}

		/// <summary>
		/// Retrieves a string representation of a list putting together all the parameters passed to the method in an object array
		/// </summary>
		/// <param name="info">The method for which to generate the argument array string</param>
		/// <returns>ie new object[]{value, argument}</returns>
		private static string GetArgumentArray(MethodInfo info)
		{
			string arguments = "new object[]{";
			foreach (ParameterInfo parameter in info.GetParameters())
				arguments += parameter.Name + ", ";

			// Remove last comma and space if any parameters where added and add the last clammer
			return arguments.TrimEnd(',', ' ') + "}";
		}

		/// <summary>
		/// Retrieves the argument list
		/// </summary>
		/// <param name="methodInfo">The method to generate the argument from</param>
		/// <param name="neededTypes">A list to which the methods needed types can be added</param>
		/// <returns>Ie GetData(int arg1, string arg4) => Int32 arg1, String arg4</returns>
		private static string GetArgumentDeclaration(MethodInfo methodInfo, ICollection<Type> neededTypes)
		{
			string arguments = string.Empty;
			foreach (ParameterInfo info in methodInfo.GetParameters())
			{
				arguments += _typeNameGenerator.GetGenericName(info.ParameterType) + " " + info.Name + ", ";
				
				if(!neededTypes.Contains(info.ParameterType))
					neededTypes.Add(info.ParameterType);
			}

			return arguments.TrimEnd(',', ' ');
		}

		#endregion

		/// <summary>
		/// Retrieves a string representing the call to Type.GetMethods needed to retrieve the method
		/// </summary>
		/// <returns>
		/// ie. GetMethod("GetUserName", new Type[]{typeof(Int32), typeof(String)});
		/// </returns>
		public string GetMethodCall
		{
			get { return _getMethodCall; }
		}

		/// <summary>
		/// Retrieves a string representation of the method return type
		/// </summary>
		public string ReturnType
		{
			get { return _returnType; }
		}

		/// <summary>
		/// Retrieves the name of a method
		/// </summary>
		public string Name
		{
			get { return _name; }
		}

		/// <summary>
		/// Retrieves a signature for the method, ie _getValuesInt32String
		/// </summary>
		public string Signature
		{
			get { return _signature; }
		}

		/// <summary>
		/// Retrieves a string for declaring the ObjectProxy method in the form <see cref="MethodDeclaration"/>
		/// </summary>
		public string Declaration
		{
			get { return _declaration; }
		}

		/// <summary>
		/// Retrieves a list of types the method depends on
		/// </summary>
		public List<Type> NeededTypes
		{
			get { return _neededTypes; }
        }

#if TEST
        #region Tests

		[Fact]
		private static void TestNormalMethod()
		{
			MethodInfo methodInfo = typeof (TestObject).GetMethod("TestIt", new Type[] {typeof (string), typeof (int)});
			ObjectProxyMethod method = new ObjectProxyMethod(methodInfo);

			Assert.Equal("_testItStringInt32", method.Signature);
			Assert.Equal("TestIt", method.Name);
			Assert.Equal("String", method.ReturnType);
			Assert.Equal("GetMethod(\"TestIt\", new Type[]{typeof(String), typeof(Int32)});", method.GetMethodCall);

			methodInfo = typeof(TestObject).GetMethod("DoThis");
			method = new ObjectProxyMethod(methodInfo);
			Assert.Equal("Void", method.ReturnType);
			Assert.Equal(@"
		public override void DoThis()
		{
			ObjectProxyCommand cmd = new ObjectProxyCommand(_realType, _doThis, new object[]{});
			_dispatcher.Invoke(this, cmd);
			if (!cmd.IsHandled)
				throw new NotImplementedException(""Fadd.Commands.Tests.TestObject.DoThis have not been implemented."");
			if (cmd.Exception != null)
				throw cmd.Exception;
		}", method.Declaration);
		}

		[Fact]
		private static void TestGenericMethod()
		{
			MethodInfo methodInfo = typeof(GenericTestObject<int, List<string>>).GetMethod("RetrieveGenericDictionary");
			ObjectProxyMethod method = new ObjectProxyMethod(methodInfo);

			Assert.Equal("_retrieveGenericDictionaryEventHandlerEventArgs", method.Signature);
			Assert.Equal("RetrieveGenericDictionary", method.Name);
			Assert.Equal("Dictionary<Q, P>", method.ReturnType);
			Assert.Equal(@"
		public override Dictionary<Q, P> RetrieveGenericDictionary<Q, P>(EventHandler<EventArgs> handler)
		{
			ObjectProxyCommand cmd = new ObjectProxyCommand(_realType, _retrieveGenericDictionaryEventHandlerEventArgs, new object[]{handler});
			_dispatcher.Invoke(this, cmd);
			if (!cmd.IsHandled)
				throw new NotImplementedException(""Fadd.Commands.Tests.GenericTestObject.RetrieveGenericDictionary have not been implemented."");
			if (cmd.Exception != null)
				throw cmd.Exception;

			return (Dictionary<Q, P>)cmd.ReturnValue;
		}", method.Declaration);
		}

        #endregion
#endif
    }
}
